package top.ieei.demo.tftp;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * 处理TFTP的读写请求的套接字封装<br>
 * @author xiecongshu
 */
public class TftpSocket
{
    /**
     * 日志
     */
    private static final Log LOG = LogFactory.getLog(TftpSocket.class);
    
    /**
     * 默认收发数据块大小
     */
    public static final int BLOCK_SIZE = 512;
    
    /**
     * 处理端口
     */
    private static int handlePort;
    
    /**
     * 超时时间，毫秒
     */
    private int timeout;
    
    /**
     * 数据缓冲
     */
    protected byte[] buffer;
    
    /**
     * 目的地址
     */
    protected InetAddress destAddr;
    
    /**
     * 目的端口
     */
    protected int destPort;
    
    /**
     * 用来发送和接收数据报包的套接字
     */
    protected DatagramSocket udpSocket;
    
    /**
     * <默认构造函数>
     * @param timeout 超时时间 单位毫秒
     */
    public TftpSocket(int timeout) throws SocketException
    {
        udpSocket = getFreeSocket();
        this.setSockTimeout(timeout);
        buffer = new byte[BLOCK_SIZE + 16];
    }
    
    /**
     * 设置超时时间，单位毫秒
     */
    public void setSockTimeout(int timeout) throws SocketException
    {
        this.timeout = timeout;
        udpSocket.setSoTimeout(timeout);
    }
    
    public int getSocketTimeOut()
    {
        return timeout;
    }
    
    /**
     * 获取空闲端口创建数据报套接字实例
     * @return
     * @throws SocketException DatagramSocket
     */
    private static DatagramSocket getFreeSocket() throws SocketException
    {
        int loopPort = handlePort - 1;
        while (loopPort != handlePort)
        {
            if ((handlePort < 29001) || (++handlePort > 65000))
            {
                handlePort = 29001;
            }
            try
            {
                InetAddress address = InetAddress.getByName(TftpServer.tftpIp);
                DatagramSocket freeSocket = new DatagramSocket(handlePort, address);
                return freeSocket;
            }
            catch (SocketException e)
            {
                continue;
            }
            catch (Exception e)
            {
                continue;
            }
        }
        LOG.error("TftpSocket could not find a free port!");
        throw new SocketException();
    }
    
    /**
     * 读取UDP数据包数据封装到TftpPacket
     * @return TftpPacket
     * @throws IOException
     * @throws TftpException 
     */
    public TftpPacket read() throws IOException, TftpException
    {
        DatagramPacket udpPacket = new DatagramPacket(buffer, BLOCK_SIZE + 16);
        try
        {
            udpSocket.receive(udpPacket);
            udpSocket.setSoTimeout(3000);
            
        }
        catch (InterruptedIOException e)
        {
            // 超时，未收到数据包
            return null;
        }
        
        byte[] udpData = udpPacket.getData();
        int udpLength = udpPacket.getLength();
        LOG.debug("udpPacket.length in receive: " + udpLength);
        
        // 复制数据到TftpPacket
        byte[] tftpBytes = new byte[udpPacket.getLength()];
        System.arraycopy(udpData,
                udpPacket.getOffset(),
                tftpBytes,
                0,
                udpLength);
        
        TftpPacket tftpPacket = null;
        try
        {
            int opCode = TftpPacket.fetchOpCode(tftpBytes);
            switch (opCode)
            {
                case TftpAck.OPCODE:
                    tftpPacket = new TftpAck(tftpBytes);
                    LOG.debug("udpPacket receive TftpAck.");
                    break;
                case TftpData.OPCODE:
                    tftpPacket = new TftpData(tftpBytes, udpLength);
                    LOG.debug("udpPacket receive TftpData.");
                    break;
                case ReadRequest.OPCODE:
                    tftpPacket = new ReadRequest(tftpBytes);
                    LOG.debug("udpPacket receive ReadRequest.");
                    break;
                case WriteRequest.OPCODE:
                    tftpPacket = new WriteRequest(tftpBytes);
                    LOG.debug("udpPacket receive WriteRequest.");
                    break;
                case TftpOptionAck.OPCODE:
                    tftpPacket = new TftpOptionAck(tftpBytes);
                    LOG.debug("udpPacket receive TftpOptionAck.");
                    break;
                case TftpError.OPCODE:
                    tftpPacket = new TftpError(tftpBytes);
                    LOG.debug("udpPacket receive TftpError.");
                    break;
                default:
                    LOG.error("Receive unknown opCode: " + opCode);
                    break;
                
            }
        }
        catch (Exception e)
        {
            if (e instanceof TftpException)
            {
                throw (TftpException) e;
            }
            throw new IOException(
                    "Could not discover tftp packet in recieved data!"
                            + e.getMessage());
        }
        tftpPacket.setPort(udpPacket.getPort());
        tftpPacket.setAddress(udpPacket.getAddress());
        
        return tftpPacket;
    }
    
    /**
     * 发送UDP数据包
     * @param tftpBytes
     * @throws IOException 
     */
    public void write(TftpPacket tftpBytes) throws IOException
    {
        byte[] data = tftpBytes.getBytes();
        InetAddress address = tftpBytes.getAddress();
        
        int port = tftpBytes.getPort();
        
        if (udpSocket.isConnected())
        {
            address = destAddr;
            port = destPort;
        }
        
        DatagramPacket udpPacket = new DatagramPacket(data, data.length,
                address, port);
        udpSocket.send(udpPacket);
    }
    
    /**
     * 打开到目的地址的连接
     * @param addr
     * @param port void
     */
    public void connect(InetAddress addr, int port)
    {
        udpSocket.disconnect();
        udpSocket.connect(addr, port);
        destAddr = addr;
        destPort = port;
    }
    
    /**
     * 清空DatagramSocket
     */
    public void clear()
    {
        try
        {
            udpSocket.setSoTimeout(10);
        }
        catch (SocketException e)
        {
            LOG.error("Clear DatagramSocket,udpSocket setSoTimeout fail. "
                    + e.getMessage());
        }
        byte[] data = new byte[516];
        DatagramPacket udpPacket = new DatagramPacket(data, data.length);
        while (true)
        {
            try
            {
                udpSocket.receive(udpPacket);
            }
            catch (SocketTimeoutException ste)
            {
                try
                {
                    udpSocket.setSoTimeout(timeout);
                }
                catch (SocketException e1)
                {
                    LOG.error("Clear DatagramSocket,udpSocket setSoTimeout fail. "
                            + e1.getMessage());
                }
                return;
            }
            catch (Exception e)
            {
                LOG.error("Clear DatagramSocket error. " + e.getMessage());
                try
                {
                    udpSocket.setSoTimeout(timeout);
                }
                catch (SocketException e1)
                {
                    LOG.error("Clear DatagramSocket,udpSocket setSoTimeout fail. "
                            + e1.getMessage());
                }
                return;
            }
        }
        
    }
    
    /**
     * 断开DatagramSocket连接
     */
    public void disconnect()
    {
        udpSocket.disconnect();
        udpSocket.close();
    }
    
    /**
     * 获取目录端口
     * @return int
     */
    public int getPort()
    {
        return destPort;
    }
    
    /**
     * 获取目的地址
     * @return InetAddress
     */
    public InetAddress getAddress()
    {
        return destAddr;
    }
}
